package com.meyou.javascript.context;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;

public class MeyouContext {

	private static final long serialVersionUID = 1L;
	
	private Context ctx;
	
	private boolean ctxInited;
	
	private ScriptableObject scriptable;
	
	private ContextFactory factory = new ContextFactory();
	
	public MeyouObject getGlobalObject()
	{
		try
		{
			enterContext();
			
			return new MeyouObject(scriptable);
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject getProperty(MeyouObject meyouObj, String name)
	{
		try
		{
			enterContext();
			
			Scriptable scrp = meyouObj.asScriptable();
			
			Object resultObj = ScriptableObject.getProperty(scrp, name);
			
			MeyouObject meyouResultObj = new MeyouObject(resultObj);
			
			return meyouResultObj;
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject getIndexedProperty(MeyouObject meyouObj, int index)
	{
		try
		{
			enterContext();
			
			Scriptable scrp = meyouObj.asScriptable();
			
			Object resultObj = ScriptableObject.getProperty(scrp, index);
			
			MeyouObject meyouResultObj = new MeyouObject(resultObj);
			
			return meyouResultObj;
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public void setProperty(MeyouObject meyouObj, String name, MeyouObject valueMeYouObj)
	{
		try
		{
			enterContext();
			
			Scriptable scrp = meyouObj.asScriptable();
			
			scrp.put(name, scrp, valueMeYouObj.asObject());
		}
		finally
		{
			exitContext();
		}
	}
	
	public void setIndexedProperty(MeyouObject meyouObj, int idx, MeyouObject valueMeYouObj)
	{
		try
		{
			enterContext();
			
			Scriptable scrp = meyouObj.asScriptable();
			
			scrp.put(idx, scrp, valueMeYouObj.asObject());
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject createInteger(Integer value)
	{
		MeyouObject meyouObj = new MeyouObject(value);
		
		return meyouObj;
	}
	
	public MeyouObject createString(String value)
	{
		MeyouObject meyouObj = new MeyouObject(value);
		
		return meyouObj;
	}
	
	public MeyouObject createArray(Object[] elements)
	{
		try
		{
			enterContext();
			
			Scriptable scr = ctx.newArray(scriptable, elements);
			
			MeyouObject meyouObj = new MeyouObject(scr);
			
			return meyouObj;
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject createNativeObject(Map<Object, Object> map)
	{
		try
		{
			enterContext();
		
			NativeObject scr = (NativeObject) ctx.newObject(scriptable);
			
			MeyouObject meyouObj = new MeyouObject(scr);
			
			for (Object key : map.keySet())
			{
				if (key == null) continue;
				
				Object value = map.get(key);
				
				setProperty(meyouObj, key.toString(), new MeyouObject(value));
			}
			
			return meyouObj;
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject createFunction(final String name, final Object[] args, final MeyouCallback callback)
	{
		try
		{
			enterContext();
			
			MeyouFunction meyouFunction = new MeyouFunction(name, args, callback);
			
			MeyouObject meyouFunctionObj = new MeyouObject(meyouFunction);
			
			return meyouFunctionObj;
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public boolean isArrayEmpty(MeyouObject meyouObj)
	{
		try
		{
			enterContext();
			
			MeyouObject meyouResultObj = getProperty(meyouObj, "length");
			
			return meyouResultObj.asInteger() == 0;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject popArray(MeyouObject meyouObj)
	{
		try
		{
			enterContext();
			
			MeyouObject meyouResultObj = invokeMethod(meyouObj, "pop", new Object[0]);
			
			return meyouResultObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject pushArray(MeyouObject meyouObj)
	{
		try
		{
			enterContext();
			
			MeyouObject meyouResultObj = invokeMethod(meyouObj, "push", new Object[0]);
			
			return meyouResultObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject invokeMethod(MeyouObject meyouObj, String methodName, Object[] args)
	{
		try
		{
			enterContext();
			
			Scriptable scrpt = meyouObj.asScriptable();
			
			Object resultObj = NativeObject.callMethod(ctx, scrpt, methodName, args);
			
			MeyouObject resultMeYouObj = new MeyouObject(resultObj);
			
			return resultMeYouObj;
			
		}
		finally
		{
			exitContext();
		}
		
	}
	
	public MeyouObject runScript(MeyouObject meyouObj)
	{
		try
		{
			enterContext();
			
			Script scr = meyouObj.asScript();
			
			Object result = scr.exec(ctx, scriptable);
			
			MeyouObject resultMeyouObj = new MeyouObject(result);
			
			return resultMeyouObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject runFunction(MeyouObject meyouObj, Object[] args)
	{
		try
		{
			enterContext();
			
			Function func = meyouObj.asFunction();
			
			Object result = func.call(ctx, scriptable, func, args);
			
			MeyouObject resultMeyouObj = new MeyouObject(result);
			
			return resultMeyouObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject compileScript(String source)
	{
		try
		{
			enterContext();
			
			Object obj = ctx.compileString(source, "<meyou/>", 0, null);
			
			MeyouObject meyouObj = new MeyouObject(obj);
			
			return meyouObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public MeyouObject compileFunction(String source)
	{
		try
		{
			enterContext();
			
			Function func = ctx.compileFunction(scriptable, source, "<meyou/>", 0, null);
			
			MeyouObject meyouObj = new MeyouObject(func);
			
			return meyouObj;
		}
		finally
		{
			exitContext();
		}
	}
	
	public void importJavaLibraryPackages(String[] pkgs)
	{
		for (String pkg : pkgs)
		{
			String str = "importPackage(" + pkg + ");";
			
			MeyouObject obj = compileScript(str);
			
			runScript(obj);
		}
	}
	
	public void importPackages(String[] pkgs)
	{
		for (String pkg : pkgs)
		{
			String str = "importPackage(" + "Packages." + pkg + ");";
			
			MeyouObject obj = compileScript(str);
			
			runScript(obj);
		}
	}
	
	public Map toJavaMap(MeyouObject meyouObj)
	{
		Scriptable jsObj = meyouObj.asScriptable();
		
		Object[] ids = jsObj.getIds();
		
		Map map = new HashMap();
		
		for (Object id : ids)
		{
			Object value = jsObj.get(id.toString(), jsObj);
			
			map.put(id, value);
		}
		
		return map;
	}
	
	public Object[] toJavaArray(MeyouObject meyouObj)
	{
		Scriptable jsObj = meyouObj.asScriptable();
		
		Object[] ids = jsObj.getIds();
		
		Object[] arr = new Object[ids.length];
		
		for (Object id : ids)
		{
			int idx = ((Number) id).intValue();
			
			Object value = jsObj.get(idx, jsObj);
			
			arr[idx] = value;
		}
		
		return arr;
	}
	
	public List toJavaList(MeyouObject meyouObj)
	{
		Scriptable jsObj = meyouObj.asScriptable();
		
		Object[] ids = jsObj.getIds();
		
		int num = ids.length;
		
		List list = new ArrayList(num);
		
		for (int i = 0; i < num; i++)
		{
			list.add(null);
		}
		
		for (Object id : ids)
		{
			int idx = ((Number) id).intValue();
			
			Object value = jsObj.get(idx, jsObj);
			
			list.set(idx, value);
		}
		
		return list;
	}
	
	public Set toJavaSet(MeyouObject meyouObj)
	{
		Scriptable jsObj = meyouObj.asScriptable();
		
		Object[] ids = jsObj.getIds();
		
		Set set = new LinkedHashSet();
		
		for (Object id : ids)
		{
			int idx = ((Number) id).intValue();
			
			Object value = jsObj.get(idx, jsObj);
			
			set.add(value);
		}
		
		return set;
	}
	
	public MeyouObject toJs(Map javaObjAsMap)
	{
		MeyouObject meyouObj = createNativeObject(javaObjAsMap);
		
		return meyouObj;
	}
	
	public MeyouObject toJs(Object[] javaObjAsObjArr)
	{
		MeyouObject meyouObj = createArray(javaObjAsObjArr);
		
		return meyouObj;
	}
	
	public MeyouObject toJs(Collection javaObjAsColl)
	{
		Object[] javaObjAsObjArr = javaObjAsColl.toArray(new Object[0]);
		
		MeyouObject meyouObj = createArray(javaObjAsObjArr);		
		
		return meyouObj;
	}
	
	public MeyouObject inMeyou(Object obj)
	{
		return new MeyouObject(obj);
	}
	
	public MeyouObject wrap(Object javaObject)
	{
		try
		{
			enterContext();
			
			Object wrapObj = Context.javaToJS(javaObject, scriptable);
			
			MeyouObject meyouWrapObject = new MeyouObject(wrapObj);
			
			return meyouWrapObject;
		}
		finally
		{
			exitContext();
		}
	}
	
	public void defineClass(Class<? extends ScriptableObject> claz)
	{
		try
		{
			enterContext();
			
			try
			{
				ScriptableObject.defineClass(scriptable, claz);
			}
			catch(InvocationTargetException e)
			{
				e.printStackTrace();
			}
			catch(InstantiationException e)
			{
				e.printStackTrace();
			}
			catch(IllegalAccessException e)
			{
				e.printStackTrace();
			}
			
		}
		finally
		{
			exitContext();
		}
	}
	
	public void close()
	{
		ctx = null;
	}
	
	protected Context enterContext()
	{
		ctx = factory.enterContext(ctx);
		
		if (!ctxInited)
		{
			scriptable = new MeyouScriptableObject(ctx);
			
			ctx.initStandardObjects(scriptable);
			
			ctxInited = true;
		}
		
		return ctx;
		
	}
	
	protected void exitContext()
	{
		Context.exit();
	}
	
	public static class MeyouObject
	{
		Object jsObj;
		
		MeyouObject(Object jsObj)
		{
			this.jsObj = jsObj;
		}
		
		public boolean isNull()
		{
			return (jsObj == null);
		}
		
		public boolean isUndefined()
		{
			return (jsObj instanceof Undefined);
		}
		
		public Integer asInteger()
		{
			if (isNull())
				return null;
			
			Number num = asNumber();
			
			return num.intValue();
		}
		
		public Double asDouble()
		{
			if (isNull())
				return null;
			
			Number num = asNumber();
			
			return num.doubleValue();
		}
		
		public Number asNumber()
		{
			return (Number) jsObj;
		}
		
		public Date asDate()
		{
			return (Date) jsObj;
		}
		
		public String asString()
		{
			return (String) jsObj;
		}
		
		public Object asObject()
		{
			if (jsObj instanceof NativeJavaObject)
			{
				NativeJavaObject nativeJavaObj = (NativeJavaObject) jsObj;
				
				return nativeJavaObj.unwrap();
			}
			
			return jsObj;
		}
		
		NativeObject asNativeObject()
		{
			return (NativeObject) jsObj;
		}
		
		NativeArray asNativeArray()
		{
			return (NativeArray) jsObj;
		}
		
		Function asFunction()
		{
			return (Function) jsObj;
		}
		
		Script asScript()
		{
			return (Script) jsObj;
		}
		
		Scriptable asScriptable()
		{
			return (Scriptable) jsObj;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((jsObj == null) ? 0 : jsObj.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			MeyouObject other = (MeyouObject) obj;
			if (jsObj == null) {
				if (other.jsObj != null)
					return false;
			} else if (!jsObj.equals(other.jsObj))
				return false;
			return true;
		}
		
	}
	
	static class MeyouScriptableObject extends ImporterTopLevel
	{

		private static final long serialVersionUID = 1L;

		public MeyouScriptableObject(Context ctx) {
			super(ctx);
		}

		@Override
		public String getClassName() {
			return "global";
		}
		
	}
	
	static class MeyouFunction extends BaseFunction
	{

		private static final long serialVersionUID = 1L;
		
		private String name;
		
		private MeyouCallback callback;
		
		public MeyouFunction(String name, Object args, MeyouCallback callback) 
		{
			super();
			
			this.name = name;
			this.callback = callback;
			
			//Id_arguments = 5
			setInstanceIdValue(5, args);
		}
		
		@Override
		public String getFunctionName()
	    {
	        return name;
	    }
		
		@Override
		public Object call(Context cx, Scriptable scope, Scriptable thisObj,
                   Object[] args)
	    {
	        return callback.onComplete(args);
	    }

	}

}
